fix(tip-1034): sync escrow implementation with spec#3837
fix(tip-1034): sync escrow implementation with spec#3837legion2002 merged 2 commits intotanishk/tip-1034-implfrom
Conversation
|
1a2ffb4 to
b17b014
Compare
tempoxyz-cyclops-bot
left a comment
There was a problem hiding this comment.
👁️ Cyclops Review
PR #3837 updates the TIP-1034 channel-escrow precompile to match the spec. The escrow code is gated behind T5 (T2 is currently active), so all impacts described below are latent until T5 activates.
Head drift note: consolidation was scheduled against
e4d0fc0b313b; current head isb17b0142…. Both findings re-checked against the new head and still apply (the field was renamedexpiring_nonce_hash→channel_open_context_hashand moved to top-levelTempoTxEnv; the RPC sentinel changed fromSome(B256::ZERO)toNone, but the handler skips seeding the precompile slot when the hash isNone, soopen()still reverts in simulation).
⚠️ [ISSUE] Channel-escrow lifecycle calls are still classified as non-payment (file outside this diff)
Severity: Medium (latent until T5; spec-vs-impl divergence today)
Files (not in this PR's diff):
crates/primitives/src/transaction/envelope.rs:174-225(is_payment_v1,is_payment_v2)crates/primitives/src/transaction/envelope.rs:475-485(is_tip20_call,is_tip20_payment)crates/transaction-pool/src/transaction.rs:61-65(cachedis_payment_v2flag)crates/payload/builder/src/lib.rs:480-493(non-paymentgeneral_gas_limitskip)crates/evm/src/block.rs:410-414(consensus non-payment gas-left handling)
Summary: TIP-1034 §"Payment-Lane Integration (Mandatory)" (tips/tip-1034.md:259-290, 313-314) requires escrow lifecycle calls (open, settle, topUp, close, requestClose, withdraw) to be recognized by is_payment_v1 (consensus) and is_payment_v2 (pool/builder) via an is_channel_escrow_payment(to, input) helper. Neither classifier was updated; both still match only the TIP-20 0x20c0… prefix. Because TIP20_CHANNEL_ESCROW_ADDRESS = 0x4D5050… does not satisfy that prefix, every syntactically valid escrow call is classified as non-payment, and the misclassification is consumed by the pool's cached is_payment flag, the payload builder's non-payment gas-limit gate, and consensus block sectioning.
Impact (post-T5): During the 15-minute close-grace window, a payee's settle()/close() is no longer protected by payment-lane capacity reservation; congestion in the non-payment lane lets the payer's withdraw() race past the grace boundary. This breaks the close-grace economic invariant TIP-1034 explicitly relies on.
Recommended Fix: Implement is_channel_escrow_payment(to, input) (selector + ABI-length + Tempo-signature-shape checks) and wire it into both is_payment_v1() and is_payment_v2(). Preserve the existing side-effect exclusions (authorization_list, tempo_authorization_list, key_authorization, empty AA call list). Add regression tests across Legacy / EIP-2930 / EIP-1559 / EIP-7702 / AA envelopes plus pool and builder integration. The PR's commit history shows this was deliberately split off as a follow-up, but the spec invariant is asserted as MUST in the merged TIP text.
Reviewer Callouts
- ⚡ Sentinel-collision pattern in RPC simulation envs:
crates/alloy/src/rpc/reth_compat.rsstill uses several "zero is harmless" placeholders (signature_hash: B256::ZERO,tx_hash: B256::ZERO,expiring_nonce_hash: Some(B256::ZERO)insideTempoBatchCallEnv). The first finding shows the invariant is fragile; any new precompile consumer of these fields must explicitly handle zero. Worth auditing every future use oftempo_tx_env.{tx_hash, signature_hash, expiring_nonce_hash}for the same trap. - ⚡ Transient layout WARNING is unenforced: the
// WARNING:comment oncrates/precompiles/src/tip20_channel_escrow/mod.rs:62-64says transient slots must remain after persistent slots until thecontractmacro supports independent layouts. There is no compile-time check; consider splitting the transient fields into a separate struct now. - ⚡ Voucher signature gas charged via internal Rust call, not via a TIP-1020 precompile CALL:
validate_voucherconstructs aSignatureVerifier::new()and calls.recover(...)directly, deductingSECP256K1_VERIFY_GAS/P256_VERIFY_GAS/WEBAUTHN_VERIFY_GASthrough the sharedStorageCtx. Cryptographic rules are identical, but a strict reading of TIP-1034 rule 1 ("verify via the TIP-1020 precompile") might flag the gas divergence (the calldata cost of an actual external CALL is not paid). - ⚡ Per-token TIP-403 dependency: every outbound leg (
settle/close/withdraw) callssystem_transfer_from(self.address, payer/payee, …), which goes throughvalidate_transfer→ensure_transfer_authorized(escrow, recipient). Tokens whose TIP-403 policy does not whitelistTIP20_CHANNEL_ESCROW_ADDRESSas a sender will not be usable with the escrow. Operators integrating new TIP-20 tokens with the escrow must remember to whitelist the escrow address as both sender and recipient. - ⚡ Access keys +
open: an access key with a broad call scope that includesTIP20_CHANNEL_ESCROW_ADDRESScan spend up to the master account's TIP-20 spending limit by opening a channel where a colluding address is thepayee/authorizedSigner. Spending limits are correctly applied atopen; worth a user-facing docs note when granting access-key call scopes. - ⚡
test_dispatch_rejects_static_mutationintip20_channel_escrow/mod.rsdoes not actually exercise a static call; it only checks thatescrow.call(...)succeeds in a non-static context. Static rejection is enforced bymutate/mutate_voidand tested elsewhere, but the test name is misleading. - ⚡
top_upwithadditionalDeposit = 0is legal per spec — it lets the payer cancel a pending close request without adding any funds. Not a security issue, but indexers should be aware these zero-amount events are intentional.
|
|
||
| fn enclosing_channel_open_context_hash(&self) -> Result<B256> { | ||
| let hash = self.channel_open_context_hash.t_read()?; | ||
| if hash.is_zero() { |
There was a problem hiding this comment.
🚨 [SECURITY] RPC simulation of TIP20ChannelEscrow.open() always reverts with ExpiringNonceHashNotSet
Severity: Medium (latent until T5)
The RPC pipeline (eth_call / eth_estimateGas) builds a TempoTxEnv via TempoTransactionRequest::try_into_tx_env, which hard-codes channel_open_context_hash: None (crates/alloy/src/rpc/reth_compat.rs:135). The handler's seed_precompile_tx_context only forwards the hash to TIP20ChannelEscrow::set_channel_open_context_hash when it is Some(_) (crates/revm/src/handler.rs:418-421), so the precompile's transient slot is never written and reads as zero. enclosing_channel_open_context_hash here treats the zero TLOAD as "unset" and returns expiring_nonce_hash_not_set, so every simulated open() reverts even though the same call would succeed on-chain (where crates/revm/src/tx.rs populates a non-zero keccak256(encode_for_signing || sender) for every real tx type).
Impact: eth_estimateGas for open() either errors or binary-searches on a path that never produces a successful open; eth_call previewing the returned bytes32 channelId always errors with ExpiringNonceHashNotSet; wallets/SDKs that simulate before submitting will refuse otherwise-valid open txs once T5 is active.
Recommended Fix: Either (a) compute a deterministic non-zero channel_open_context_hash for the simulation env in reth_compat.rs (mirroring the real formula or substituting any non-zero placeholder, then documenting that any non-zero value is acceptable), or (b) distinguish "set" from "not set" in the precompile via an explicit transient flag rather than the hash.is_zero() sentinel. Option (a) is the smaller and more direct fix.
b17b014 to
d70f4b6
Compare
📊 Tempo Precompiles CoverageprecompilesCoverage: 5634/8218 lines (68.56%) File details
contractsCoverage: 1/274 lines (0.36%) File details
Total: 5635/8492 lines (66.36%) |
Stacks on #3704.\n\nSyncs the Rust TIP-1034 escrow precompile and ABI with the updated spec/reference implementation: expiring-nonce channel IDs, operator settlement, terminal state deletion, and same-transaction reopen protection.\n\nAdds focused coverage for the escrow spec drift. Payment-lane classifier changes are intentionally left out and will land in a separate PR.